package com.zbs.framework.dao.utils.jpa;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.query.QueryUtils;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.*;
import java.util.*;

/**
 * @author: Zbs
 * @create: 2020-09-10 14:31
 * @description: SpecificationHelper 工具类
 */
public class SpecificationHelper<T> {

    public List<T> findResult(EntityManager entityManager, Operator params, Class<T> t){
        return this.findResult(entityManager,params,t,null).getContent();
    }


    private TypedQuery<T> getTypedQuery(EntityManager entityManager, Operator params, Class<T> t, Pageable pageable){
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<T> cq = cb.createQuery(t);
        Root<T> root = cq.from(t);
        if (!CollectionUtils.isEmpty(params.getSelectors())){
            Selection<?>[] selections = new Selection[params.getSelectors().size()];
            for (int i = 0; i < params.getSelectors().size(); i++) {
                selections[i] =root.get(params.getSelectors().get(i).getColumn());
            }
            cq.select(cb.construct(t,selections));
        }

        Predicate predicate = cb.conjunction();
        List<Predicate> or = new ArrayList<>();
        List<Predicate> and = new ArrayList<>();
        for (Operator param : params.getCriteria()) {
            if (param.getJoin() == Operator.Join.or) {
                or.add(this.getPredicate(root, cb, param));
            } else {
                and.add(this.getPredicate(root, cb, param));
            }
        }

        Map<Integer, List<Operator>> orOperator = params.getOrOperator();
        if (!CollectionUtils.isEmpty(orOperator)){
            List<Predicate> or2 = new ArrayList<>();
            orOperator.keySet().forEach(key -> {
                List<Operator> operators = orOperator.get(key);
                Predicate[] predicates = operators.stream().map(operator -> this.getPredicate(root, cb, operator)).toArray(Predicate[]::new);
                or2.add(cb.and(predicates));
            });
            predicate.getExpressions().add(cb.or(or2.toArray(new Predicate[or2.size()])));
        }

        if (!CollectionUtils.isEmpty(or)) {
            predicate.getExpressions().add(cb.or(or.toArray(new Predicate[or.size()])));
        }

        if (!CollectionUtils.isEmpty(and)) {
            predicate.getExpressions().addAll(and);
        }

        if (pageable != null && pageable.getSort().isSorted()) {
            cq.orderBy(QueryUtils.toOrders(pageable.getSort(), root, cb));
        }
        cq.where(predicate);
        return entityManager.createQuery(cq);
    }

    public Page<T> findResult(EntityManager entityManager, Operator params, Class<T> t, Pageable pageable) {
        TypedQuery<T> query = this.getTypedQuery(entityManager, params, t, pageable);
        if (pageable != null){
            query.setFirstResult(pageable.getPageNumber() * pageable.getPageSize());
            query.setMaxResults(pageable.getPageSize());

            return PageableExecutionUtils.getPage(query.getResultList(), pageable,
                    () -> executeCountQuery(getCountQuery(entityManager,params, t)));

//            return PageableExecutionUtils.getPage(query.getResultList(), pageable,
//                    ()-> executeCountQuery(getCountQuery(entityManager,getPacification(params),t)));
        }else {
            return new PageImpl<>(query.getResultList());
        }
    }

    public long count(EntityManager entityManager, Operator params, Class<T> t) {
        return executeCountQuery(this.getCountQuery(entityManager,params,t));
    }




    /**
     * 计算总数
     * @param query
     * @return
     */
    private static long executeCountQuery(TypedQuery<Long> query) {
        Assert.notNull(query, "TypedQuery must not be null!");
        List<Long> totals = query.getResultList();
        long total = 0L;

        for (Long element : totals) {
            total += element == null ? 0 : element;
        }
        return total;
    }

    /**
     * 获取总数
     * @param entityManager
     * @param params
     * @param domainClass
     * @return
     */
    private TypedQuery<Long> getCountQuery(EntityManager entityManager,Operator params, Class<T> domainClass) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Long> cq = cb.createQuery(Long.class);
        Root<T> root = cq.from(domainClass);
        if (cq.isDistinct()) {
            cq.select(cb.countDistinct(root));
        } else {
            cq.select(cb.count(root));
        }

        Predicate predicate = cb.conjunction();
        List<Predicate> or = new ArrayList<>();
        List<Predicate> and = new ArrayList<>();
        for (Operator param : params.getCriteria()) {
            if (param.getJoin() == Operator.Join.or) {
                or.add(this.getPredicate(root, cb, param));
            } else {
                and.add(this.getPredicate(root, cb, param));
            }
        }

        Map<Integer, List<Operator>> orOperator = params.getOrOperator();
        if (!CollectionUtils.isEmpty(orOperator)){
            List<Predicate> or2 = new ArrayList<>();
            orOperator.keySet().forEach(key -> {
                List<Operator> operators = orOperator.get(key);
                Predicate[] predicates = operators.stream().map(operator -> this.getPredicate(root, cb, operator)).toArray(Predicate[]::new);
                or2.add(cb.and(predicates));
            });
            predicate.getExpressions().add(cb.or(or2.toArray(new Predicate[or2.size()])));
        }

        if (!CollectionUtils.isEmpty(or)) {
            predicate.getExpressions().add(cb.or(or.toArray(new Predicate[or.size()])));
        }

        if (!CollectionUtils.isEmpty(and)) {
            predicate.getExpressions().addAll(and);
        }

        cq.orderBy(Collections.emptyList());
        cq.where(predicate);
        return entityManager.createQuery(cq);
    }

    private  Root<T> applySpecificationToCriteria(EntityManager entityManager,@Nullable Specification<T> spec, Class<T> domainClass, CriteriaQuery<Long> query) {
        Assert.notNull(domainClass, "Domain class must not be null!");
        Assert.notNull(query, "CriteriaQuery must not be null!");
        Root<T> root = query.from(domainClass);
        if (spec == null) {
            return root;
        } else {
            CriteriaBuilder builder = entityManager.getCriteriaBuilder();
            Predicate predicate = spec.toPredicate(root, query, builder);
            if (predicate != null) {
                query.where(predicate);
            }

            return root;
        }
    }


    private Specification<T> Specification(Operator params) {
        List<Predicate> predicateList = new ArrayList<>();
        return (root, cq, cb) -> {
            List<Predicate> or = new ArrayList<>();
            for (Operator param : params.getCriteria()) {
                if (param.getJoin() == Operator.Join.or) {
                    or.add(this.getPredicate(root, cb, param));
                } else {
                    predicateList.add(this.getPredicate(root, cb, param));
                }
            }
            if (!CollectionUtils.isEmpty(or)) {
                predicateList.add(cb.or(or.toArray(new Predicate[or.size()])));
            }
            return cq.where(predicateList.toArray(new Predicate[predicateList.size()])).getRestriction();
        };
    }

    private Predicate getPredicate(Root<T> root, CriteriaBuilder cb, Operator operator) {
        Path path = root.get(operator.getColumn());
        Object value = operator.getValue();
        switch (operator.getType()) {
            case equal:
                return cb.equal(path, value);
            case like:
                return cb.like(path.as(String.class), "%" + value + "%");
            case geLike:
                return cb.like(path.as(String.class), "%" + value);
            case leLike:
                return cb.like(path.as(String.class), value + "%");
            case gt:
                return cb.gt(path, (Number) value);
            case lt:
                return cb.lt(path, (Number) value);
            case ge:
                return cb.ge(path, (Number) value);
            case le:
                return cb.le(path, (Number) value);
            case notEqual:
                return cb.notEqual(path, value);
            case notLike:
                return cb.notLike(path, "%" + value + "%");
            case greaterThan:
                return cb.greaterThan(path, (Comparable) value);
            case greaterThanOrEqualTo:
                return cb.greaterThanOrEqualTo(path, (Comparable) value);
            case lessThan:
                return cb.lessThan(path, (Comparable) value);
            case lessThanOrEqualTo:
                return cb.lessThanOrEqualTo(path, (Comparable) value);
            case isNull:
                return cb.isNull(path);
            case isNotNull:
                return cb.isNotNull(path);
            case in:
                CriteriaBuilder.In in = cb.in(path);
                return in.getExpression().in((List) value);
            case notIn:
                CriteriaBuilder.In notIn = cb.in(path);
                return cb.not(notIn.getExpression().in((List) value));
            case dateBetween:
                Date[] dates = (Date[]) value;
                return cb.between(path, dates[0], dates[1]);
            case numberBetween:
                List<Long> numbers = (List) value;
                return cb.between(path, numbers.get(0), numbers.get(1));
            default:
                return null;
        }
    }

    public Specification<T> getPacification(Operator operator) {
        return this.Specification(operator);
    }
}
